/*-------------------------------------------------------------------------
 *
 * basic_archive.c
 *
 * 该文件演示了一个基本的归档库实现，大致相当于以下 shell 命令：
 *
 * 		test ! -f /path/to/dest && cp /path/to/src /path/to/dest
 *
 * 该模块与上述 shell 命令之间的一个显著区别是，该模块首先将文件复制到临时目的地，
 * 同步到磁盘，然后再将其持久移动到最终目的地。
 *
 * 另一个显著区别是，如果 /path/to/dest 已经存在
 * 但内容与 /path/to/src 相同，归档将成功，
 * 而上面显示的命令将失败。这可以防止在文件成功归档后
 * 系统崩溃之前未成功记录成功的问题。
 *
 * Copyright (c) 2022, PostgreSQL Global Development Group
 *
 * IDENTIFICATION
 *	  contrib/basic_archive/basic_archive.c
 *
 *-------------------------------------------------------------------------
 */
#include "postgres.h"

#include <sys/stat.h>
#include <sys/time.h>
#include <unistd.h>

#include "common/int.h"
#include "miscadmin.h"
#include "postmaster/pgarch.h"
#include "storage/copydir.h"
#include "storage/fd.h"
#include "utils/guc.h"
#include "utils/memutils.h"

PG_MODULE_MAGIC;

void		_PG_init(void);
void		_PG_archive_module_init(ArchiveModuleCallbacks *fc_cb);

static char *archive_directory = NULL;
static MemoryContext basic_archive_context;

static bool fc_basic_archive_configured(void);
static bool fc_basic_archive_file(const char *fc_file, const char *fc_path);
static void fc_basic_archive_file_internal(const char *fc_file, const char *fc_path);
static bool fc_check_archive_directory(char **fc_newval, void **fc_extra, GucSource fc_source);
static bool fc_compare_files(const char *fc_file1, const char *fc_file2);

/*
 * _PG_init
 *
 * 定义模块的 GUC。
 */
void _PG_init(void)
{
	DefineCustomStringVariable("basic_archive.archive_directory",
							   gettext_noop("Archive file destination directory."),
							   NULL,
							   &archive_directory,
							   "",
							   PGC_SIGHUP,
							   0,
							   fc_check_archive_directory, NULL, NULL);

	MarkGUCPrefixReserved("basic_archive");

	basic_archive_context = AllocSetContextCreate(TopMemoryContext,
												  "basic_archive",
												  ALLOCSET_DEFAULT_SIZES);
}

/*
 * _PG_archive_module_init
 *
 * 返回模块的归档回调。
 */
void _PG_archive_module_init(ArchiveModuleCallbacks *fc_cb)
{
	AssertVariableIsOfType(&_PG_archive_module_init, ArchiveModuleInit);

	fc_cb->check_configured_cb = fc_basic_archive_configured;
	fc_cb->archive_file_cb = fc_basic_archive_file;
}

/*
 * check_archive_directory
 *
 * 检查提供的归档目录是否存在。
 */
static bool fc_check_archive_directory(char **fc_newval, void **fc_extra, GucSource fc_source)
{
	struct stat fc_st;

	/*
	 * 默认值是一个空字符串，因此我们必须接受这个值。
	 * 我们的 check_configured 回调也会检查这一点，如果仍然为空，
	 * 则阻止归档进行。
	 */
	if (*fc_newval == NULL || *fc_newval[0] == '\0')
		return true;

	/*
	 * 确保文件路径不会太长。文档指出要归档的
	 * 文件名最长可以为 64 个字符。
	 */
	if (strlen(*fc_newval) + 64 + 2 >= MAXPGPATH)
	{
		GUC_check_errdetail("Archive directory too long.");
		return false;
	}

	/*
	 * 对指定的归档目录进行基本的有效性检查。它
	 * 可能会在将来的某个时刻被删除，因此我们仍然需要为它
	 * 在实际归档逻辑中不存在做好准备。
	 */
	if (stat(*fc_newval, &fc_st) != 0 || !S_ISDIR(fc_st.st_mode))
	{
		GUC_check_errdetail("Specified archive directory does not exist.");
		return false;
	}

	return true;
}

/*
 * basic_archive_configured
 *
 * 检查 archive_directory 是否不为空。
 */
static bool fc_basic_archive_configured(void)
{
	return archive_directory != NULL && archive_directory[0] != '\0';
}

/*
 * basic_archive_file
 *
 * 归档一个文件。
 */
static bool fc_basic_archive_file(const char *fc_file, const char *fc_path)
{
	sigjmp_buf	fc_local_sigjmp_buf;
	MemoryContext fc_oldcontext;

	/*
	 * 我们在自己的内存上下文中运行 basic_archive_file_internal()，以便
	 * 能够在错误恢复期间轻松重置它（从而避免内存
	 * 泄漏）。
	 */
	fc_oldcontext = MemoryContextSwitchTo(basic_archive_context);

	/*
	 * 由于归档器在异常堆栈的底部运行，
	 * ERROR 会变成 FATAL，导致归档器进程重启。
	 * 然而，在出现问题时使用 ereport(ERROR, ...) 代码简单易维护。
	 * 因此，我们创建了自己的异常处理程序来
	 * 捕获 ERROR，并在每次失败时返回 false，而不是重启归档器。
	 */
	if (sigsetjmp(fc_local_sigjmp_buf, 1) != 0)
	{
		/* 由于未使用 PG_TRY，必须手动重置错误堆栈 */
		error_context_stack = NULL;

		/* 在清理期间防止中断 */
		HOLD_INTERRUPTS();

		/* 报告错误并为下次清除 ErrorContext */
		EmitErrorReport();
		FlushErrorState();

		/* 关闭 copy_file() 或 compare_files() 剩余的任何打开文件 */
		AtEOSubXact_Files(false, InvalidSubTransactionId, InvalidSubTransactionId);

		/* 重置我们的内存上下文并切换回原来的上下文 */
		MemoryContextSwitchTo(fc_oldcontext);
		MemoryContextReset(basic_archive_context);

		/* 移除我们的异常处理程序 */
		PG_exception_stack = NULL;

		/* 现在我们可以再次允许中断 */
		RESUME_INTERRUPTS();

		/* 报告失败，以便归档器重试该文件 */
		return false;
	}

	/* 启用我们的异常处理程序 */
	PG_exception_stack = &fc_local_sigjmp_buf;

	/* 归档文件！ */
	fc_basic_archive_file_internal(fc_file, fc_path);

	/* 移除我们的异常处理程序 */
	PG_exception_stack = NULL;

	/* 重置我们的内存上下文并切换回原来的上下文 */
	MemoryContextSwitchTo(fc_oldcontext);
	MemoryContextReset(basic_archive_context);

	return true;
}

static void fc_basic_archive_file_internal(const char *fc_file, const char *fc_path)
{
	char		fc_destination[MAXPGPATH];
	char		fc_temp[MAXPGPATH + 256];
	struct stat fc_st;
	struct timeval fc_tv;
	uint64		fc_epoch;			/* 毫秒 */

	ereport(DEBUG3,
			(errmsg("archiving \"%s\" via basic_archive", fc_file)));

	snprintf(fc_destination, MAXPGPATH, "%s/%s", archive_directory, fc_file);

	/*
	 * 首先，检查文件是否已经被归档。如果它已经
	 * 存在且与我们尝试归档的文件内容相同，
	 * 我们可以返回成功（在确保文件持续到磁盘之前）。
	 * 这种情况在服务器在归档文件后崩溃但在将其 .ready 文件重命名为 .done 之前是可能的。
	 *
	 * 如果归档文件已经存在但内容不同，
	 * 可能出现了问题，因此我们将简单地失败。
	 */
	if (stat(fc_destination, &fc_st) == 0)
	{
		if (fc_compare_files(fc_path, fc_destination))
		{
			ereport(DEBUG3,
					(errmsg("archive file \"%s\" already exists with identical contents",
							fc_destination)));

			fsync_fname(fc_destination, false);
			fsync_fname(archive_directory, true);

			return;
		}

		ereport(ERROR,
				(errmsg("archive file \"%s\" already exists", fc_destination)));
	}
	else if (errno != ENOENT)
		ereport(ERROR,
				(errcode_for_file_access(),
				 errmsg("could not stat file \"%s\": %m", fc_destination)));

	/*
	 * 为临时文件选择一个足够唯一的名称，以避免
	 * 碰撞。这有助于避免在崩溃后临时
	 * 文件被遗留或另一个服务器恰好归档到相同目录时出现问题。
	 */

	gettimeofday(&fc_tv, NULL);
	if (pg_mul_u64_overflow((uint64) 1000, (uint64) fc_tv.tv_sec, &fc_epoch) ||
		pg_add_u64_overflow(fc_epoch, (uint64) (fc_tv.tv_usec / 1000), &fc_epoch))
		elog(ERROR, "could not generate temporary file name for archiving");

	snprintf(fc_temp, sizeof(fc_temp), "%s/%s.%s.%d." UINT64_FORMAT,
			 archive_directory, "archtemp", fc_file, MyProcPid, fc_epoch);

	/*
	 * 将文件复制到其临时目的地。请注意，如果临时文件已存在，将会失败。
	 */
	copy_file(unconstify(char *, fc_path), fc_temp);

	/*
	 * 将临时文件同步到磁盘并移动到其最终目的地。
	 * 请注意，这将覆盖任何现有文件，但只有在上面的 stat() 之后
	 * 其他人创建文件的情况下才可能。
	 */
	(void) durable_rename(fc_temp, fc_destination, ERROR);

	ereport(DEBUG1,
			(errmsg("archived \"%s\" via basic_archive", fc_file)));
}

/*
 * compare_files
 *
 * 返回文件内容是否相同。
 */
static bool fc_compare_files(const char *fc_file1, const char *fc_file2)
{
#define CMP_BUF_SIZE (4096)
	char		fc_buf1[CMP_BUF_SIZE];
	char		fc_buf2[CMP_BUF_SIZE];
	int			fc_fd1;
	int			fc_fd2;
	bool		fc_ret = true;

	fc_fd1 = OpenTransientFile(fc_file1, O_RDONLY | PG_BINARY);
	if (fc_fd1 < 0)
		ereport(ERROR,
				(errcode_for_file_access(),
				 errmsg("could not open file \"%s\": %m", fc_file1)));

	fc_fd2 = OpenTransientFile(fc_file2, O_RDONLY | PG_BINARY);
	if (fc_fd2 < 0)
		ereport(ERROR,
				(errcode_for_file_access(),
				 errmsg("could not open file \"%s\": %m", fc_file2)));

	for (;;)
	{
		int			fc_nbytes = 0;
		int			fc_buf1_len = 0;
		int			fc_buf2_len = 0;

		while (fc_buf1_len < CMP_BUF_SIZE)
		{
			fc_nbytes = read(fc_fd1, fc_buf1 + fc_buf1_len, CMP_BUF_SIZE - fc_buf1_len);
			if (fc_nbytes < 0)
				ereport(ERROR,
						(errcode_for_file_access(),
						 errmsg("could not read file \"%s\": %m", fc_file1)));
			else if (fc_nbytes == 0)
				break;

			fc_buf1_len += fc_nbytes;
		}

		while (fc_buf2_len < CMP_BUF_SIZE)
		{
			fc_nbytes = read(fc_fd2, fc_buf2 + fc_buf2_len, CMP_BUF_SIZE - fc_buf2_len);
			if (fc_nbytes < 0)
				ereport(ERROR,
						(errcode_for_file_access(),
						 errmsg("could not read file \"%s\": %m", fc_file2)));
			else if (fc_nbytes == 0)
				break;

			fc_buf2_len += fc_nbytes;
		}

		if (fc_buf1_len != fc_buf2_len || memcmp(fc_buf1, fc_buf2, fc_buf1_len) != 0)
		{
			fc_ret = false;
			break;
		}
		else if (fc_buf1_len == 0)
			break;
	}

	if (CloseTransientFile(fc_fd1) != 0)
		ereport(ERROR,
				(errcode_for_file_access(),
				 errmsg("could not close file \"%s\": %m", fc_file1)));

	if (CloseTransientFile(fc_fd2) != 0)
		ereport(ERROR,
				(errcode_for_file_access(),
				 errmsg("could not close file \"%s\": %m", fc_file2)));

	return fc_ret;
}
